# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.

from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml

HEADER = "# yaml-language-server: $schema=https://app.stainlessapi.com/config-internal.schema.json\n\n"

SECTION_ORDER = [
    "organization",
    "security",
    "security_schemes",
    "targets",
    "client_settings",
    "environments",
    "pagination",
    "settings",
    "openapi",
    "readme",
    "resources",
]

ORGANIZATION = {
    "name": "llama-stack-client",
    "docs": "https://llama-stack.readthedocs.io/en/latest/",
    "contact": "llamastack@meta.com",
}

SECURITY = [{}, {"BearerAuth": []}]

SECURITY_SCHEMES = {"BearerAuth": {"type": "http", "scheme": "bearer"}}

TARGETS = {
    "node": {
        "package_name": "llama-stack-client",
        "production_repo": "llamastack/llama-stack-client-typescript",
        "publish": {"npm": False},
    },
    "python": {
        "package_name": "llama_stack_client",
        "production_repo": "llamastack/llama-stack-client-python",
        "options": {"use_uv": True},
        "publish": {"pypi": True},
        "project_name": "llama_stack_client",
    },
    "kotlin": {
        "reverse_domain": "com.llama_stack_client.api",
        "production_repo": None,
        "publish": {"maven": False},
    },
    "go": {
        "package_name": "llama-stack-client",
        "production_repo": "llamastack/llama-stack-client-go",
        "options": {"enable_v2": True, "back_compat_use_shared_package": False},
    },
}

CLIENT_SETTINGS = {
    "default_env_prefix": "LLAMA_STACK_CLIENT",
    "opts": {
        "api_key": {
            "type": "string",
            "read_env": "LLAMA_STACK_CLIENT_API_KEY",
            "auth": {"security_scheme": "BearerAuth"},
            "nullable": True,
        }
    },
}

ENVIRONMENTS = {"production": "http://any-hosted-llama-stack.com"}

PAGINATION = [
    {
        "name": "datasets_iterrows",
        "type": "offset",
        "request": {
            "dataset_id": {"type": "string"},
            "start_index": {
                "type": "integer",
                "x-stainless-pagination-property": {"purpose": "offset_count_param"},
            },
            "limit": {"type": "integer"},
        },
        "response": {
            "data": {"type": "array", "items": {"type": "object"}},
            "next_index": {
                "type": "integer",
                "x-stainless-pagination-property": {"purpose": "offset_count_start_field"},
            },
        },
    },
    {
        "name": "openai_cursor_page",
        "type": "cursor",
        "request": {
            "limit": {"type": "integer"},
            "after": {
                "type": "string",
                "x-stainless-pagination-property": {"purpose": "next_cursor_param"},
            },
        },
        "response": {
            "data": {"type": "array", "items": {}},
            "has_more": {"type": "boolean"},
            "last_id": {
                "type": "string",
                "x-stainless-pagination-property": {"purpose": "next_cursor_field"},
            },
        },
    },
]

SETTINGS = {
    "license": "MIT",
    "unwrap_response_fields": ["data"],
    "file_header": "Copyright (c) Meta Platforms, Inc. and affiliates.\n"
    "All rights reserved.\n"
    "\n"
    "This source code is licensed under the terms described in the "
    "LICENSE file in\n"
    "the root directory of this source tree.\n",
}

OPENAPI = {
    "transformations": [
        {
            "command": "mergeObject",
            "reason": "Better return_type using enum",
            "args": {
                "target": ["$.components.schemas"],
                "object": {
                    "ReturnType": {
                        "additionalProperties": False,
                        "properties": {
                            "type": {
                                "enum": [
                                    "string",
                                    "number",
                                    "boolean",
                                    "array",
                                    "object",
                                    "json",
                                    "union",
                                    "chat_completion_input",
                                    "completion_input",
                                    "agent_turn_input",
                                ]
                            }
                        },
                        "required": ["type"],
                        "type": "object",
                    }
                },
            },
        },
        {
            "command": "replaceProperties",
            "reason": "Replace return type properties with better model (see above)",
            "args": {
                "filter": {
                    "only": [
                        "$.components.schemas.ScoringFn.properties.return_type",
                        "$.components.schemas.RegisterScoringFunctionRequest.properties.return_type",
                    ]
                },
                "value": {"$ref": "#/components/schemas/ReturnType"},
            },
        },
        {
            "command": "oneOfToAnyOf",
            "reason": "Prism (mock server) doesn't like one of our "
            "requests as it technically matches multiple "
            "variants",
        },
    ]
}

README = {
    "example_requests": {
        "default": {
            "type": "request",
            "endpoint": "post /v1/chat/completions",
            "params": {},
        },
        "headline": {"type": "request", "endpoint": "get /v1/models", "params": {}},
        "pagination": {
            "type": "request",
            "endpoint": "post /v1/chat/completions",
            "params": {},
        },
    }
}

ALL_RESOURCES = {
    "$shared": {
        "models": {
            "interleaved_content_item": "InterleavedContentItem",
            "interleaved_content": "InterleavedContent",
            "param_type": "ParamType",
            "safety_violation": "SafetyViolation",
            "sampling_params": "SamplingParams",
            "scoring_result": "ScoringResult",
            "system_message": "SystemMessage",
        }
    },
    "toolgroups": {
        "models": {
            "tool_group": "ToolGroup",
            "list_tool_groups_response": "ListToolGroupsResponse",
        },
        "methods": {
            "register": "post /v1/toolgroups",
            "get": "get /v1/toolgroups/{toolgroup_id}",
            "list": "get /v1/toolgroups",
            "unregister": "delete /v1/toolgroups/{toolgroup_id}",
        },
    },
    "tools": {
        "methods": {
            "get": "get /v1/tools/{tool_name}",
            "list": {"paginated": False, "endpoint": "get /v1/tools"},
        }
    },
    "tool_runtime": {
        "models": {
            "tool_def": "ToolDef",
            "tool_invocation_result": "ToolInvocationResult",
        },
        "methods": {
            "list_tools": {
                "paginated": False,
                "endpoint": "get /v1/tool-runtime/list-tools",
            },
            "invoke_tool": "post /v1/tool-runtime/invoke",
        },
    },
    "responses": {
        "models": {
            "response_object_stream": "OpenAIResponseObjectStream",
            "response_object": "OpenAIResponseObject",
        },
        "methods": {
            "create": {
                "type": "http",
                "streaming": {
                    "stream_event_model": "responses.response_object_stream",
                    "param_discriminator": "stream",
                },
                "endpoint": "post /v1/responses",
            },
            "retrieve": "get /v1/responses/{response_id}",
            "list": {"type": "http", "endpoint": "get /v1/responses"},
            "delete": {
                "type": "http",
                "endpoint": "delete /v1/responses/{response_id}",
            },
        },
        "subresources": {
            "input_items": {
                "methods": {
                    "list": {
                        "type": "http",
                        "paginated": False,
                        "endpoint": "get /v1/responses/{response_id}/input_items",
                    }
                }
            }
        },
    },
    "prompts": {
        "models": {"prompt": "Prompt", "list_prompts_response": "ListPromptsResponse"},
        "methods": {
            "create": "post /v1/prompts",
            "list": {"paginated": False, "endpoint": "get /v1/prompts"},
            "retrieve": "get /v1/prompts/{prompt_id}",
            "update": "post /v1/prompts/{prompt_id}",
            "delete": "delete /v1/prompts/{prompt_id}",
            "set_default_version": "post /v1/prompts/{prompt_id}/set-default-version",
        },
        "subresources": {
            "versions": {
                "methods": {
                    "list": {
                        "paginated": False,
                        "endpoint": "get /v1/prompts/{prompt_id}/versions",
                    }
                }
            }
        },
    },
    "conversations": {
        "models": {"conversation_object": "Conversation"},
        "methods": {
            "create": {"type": "http", "endpoint": "post /v1/conversations"},
            "retrieve": "get /v1/conversations/{conversation_id}",
            "update": {
                "type": "http",
                "endpoint": "post /v1/conversations/{conversation_id}",
            },
            "delete": {
                "type": "http",
                "endpoint": "delete /v1/conversations/{conversation_id}",
            },
        },
        "subresources": {
            "items": {
                "methods": {
                    "get": {
                        "type": "http",
                        "endpoint": "get /v1/conversations/{conversation_id}/items/{item_id}",
                    },
                    "list": {
                        "type": "http",
                        "endpoint": "get /v1/conversations/{conversation_id}/items",
                    },
                    "create": {
                        "type": "http",
                        "endpoint": "post /v1/conversations/{conversation_id}/items",
                    },
                    "delete": {
                        "type": "http",
                        "endpoint": "delete /v1/conversations/{conversation_id}/items/{item_id}",
                    },
                }
            }
        },
    },
    "inspect": {
        "models": {
            "healthInfo": "HealthInfo",
            "providerInfo": "ProviderInfo",
            "routeInfo": "RouteInfo",
            "versionInfo": "VersionInfo",
        },
        "methods": {"health": "get /v1/health", "version": "get /v1/version"},
    },
    "embeddings": {
        "models": {"create_embeddings_response": "OpenAIEmbeddingsResponse"},
        "methods": {"create": "post /v1/embeddings"},
    },
    "chat": {
        "models": {"chat_completion_chunk": "OpenAIChatCompletionChunk"},
        "subresources": {
            "completions": {
                "methods": {
                    "create": {
                        "type": "http",
                        "streaming": {
                            "stream_event_model": "chat.chat_completion_chunk",
                            "param_discriminator": "stream",
                        },
                        "endpoint": "post /v1/chat/completions",
                    },
                    "list": {
                        "type": "http",
                        "paginated": False,
                        "endpoint": "get /v1/chat/completions",
                    },
                    "retrieve": {
                        "type": "http",
                        "endpoint": "get /v1/chat/completions/{completion_id}",
                    },
                }
            }
        },
    },
    "completions": {
        "methods": {
            "create": {
                "type": "http",
                "streaming": {"param_discriminator": "stream"},
                "endpoint": "post /v1/completions",
            }
        }
    },
    "vector_io": {
        "models": {"queryChunksResponse": "QueryChunksResponse"},
        "methods": {
            "insert": "post /v1/vector-io/insert",
            "query": "post /v1/vector-io/query",
        },
    },
    "vector_stores": {
        "models": {
            "vector_store": "VectorStoreObject",
            "list_vector_stores_response": "VectorStoreListResponse",
            "vector_store_delete_response": "VectorStoreDeleteResponse",
            "vector_store_search_response": "VectorStoreSearchResponsePage",
        },
        "methods": {
            "create": "post /v1/vector_stores",
            "list": "get /v1/vector_stores",
            "retrieve": "get /v1/vector_stores/{vector_store_id}",
            "update": "post /v1/vector_stores/{vector_store_id}",
            "delete": "delete /v1/vector_stores/{vector_store_id}",
            "search": "post /v1/vector_stores/{vector_store_id}/search",
        },
        "subresources": {
            "files": {
                "models": {"vector_store_file": "VectorStoreFileObject"},
                "methods": {
                    "list": "get /v1/vector_stores/{vector_store_id}/files",
                    "retrieve": "get /v1/vector_stores/{vector_store_id}/files/{file_id}",
                    "update": "post /v1/vector_stores/{vector_store_id}/files/{file_id}",
                    "delete": "delete /v1/vector_stores/{vector_store_id}/files/{file_id}",
                    "create": "post /v1/vector_stores/{vector_store_id}/files",
                    "content": "get /v1/vector_stores/{vector_store_id}/files/{file_id}/content",
                },
            },
            "file_batches": {
                "models": {
                    "vector_store_file_batches": "VectorStoreFileBatchObject",
                    "list_vector_store_files_in_batch_response": "VectorStoreFilesListInBatchResponse",
                },
                "methods": {
                    "create": "post /v1/vector_stores/{vector_store_id}/file_batches",
                    "retrieve": "get /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}",
                    "list_files": "get /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/files",
                    "cancel": "post /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/cancel",
                },
            },
        },
    },
    "models": {
        "models": {
            "model": "OpenAIModel",
            "list_models_response": "OpenAIListModelsResponse",
        },
        "methods": {
            "list": {"paginated": False, "endpoint": "get /v1/models"},
            "retrieve": "get /v1/models/{model_id}",
            "register": "post /v1/models",
            "unregister": "delete /v1/models/{model_id}",
        },
        "subresources": {"openai": {"methods": {"list": {"paginated": False, "endpoint": "get /v1/models"}}}},
    },
    "providers": {
        "models": {"list_providers_response": "ListProvidersResponse"},
        "methods": {
            "list": {"paginated": False, "endpoint": "get /v1/providers"},
            "retrieve": "get /v1/providers/{provider_id}",
        },
    },
    "routes": {
        "models": {"list_routes_response": "ListRoutesResponse"},
        "methods": {"list": {"paginated": False, "endpoint": "get /v1/inspect/routes"}},
    },
    "moderations": {
        "models": {"create_response": "ModerationObject"},
        "methods": {"create": "post /v1/moderations"},
    },
    "safety": {
        "models": {"run_shield_response": "RunShieldResponse"},
        "methods": {"run_shield": "post /v1/safety/run-shield"},
    },
    "shields": {
        "models": {"shield": "Shield", "list_shields_response": "ListShieldsResponse"},
        "methods": {
            "retrieve": "get /v1/shields/{identifier}",
            "list": {"paginated": False, "endpoint": "get /v1/shields"},
            "register": "post /v1/shields",
            "delete": "delete /v1/shields/{identifier}",
        },
    },
    "scoring": {
        "methods": {
            "score": "post /v1/scoring/score",
            "score_batch": "post /v1/scoring/score-batch",
        }
    },
    "scoring_functions": {
        "models": {
            "scoring_fn": "ScoringFn",
            "scoring_fn_params": "ScoringFnParams",
            "list_scoring_functions_response": "ListScoringFunctionsResponse",
        },
        "methods": {
            "retrieve": "get /v1/scoring-functions/{scoring_fn_id}",
            "list": {"paginated": False, "endpoint": "get /v1/scoring-functions"},
            "register": "post /v1/scoring-functions",
            "unregister": "delete /v1/scoring-functions/{scoring_fn_id}",
        },
    },
    "files": {
        "models": {
            "file": "OpenAIFileObject",
            "list_files_response": "ListOpenAIFileResponse",
            "delete_file_response": "OpenAIFileDeleteResponse",
        },
        "methods": {
            "create": "post /v1/files",
            "list": "get /v1/files",
            "retrieve": "get /v1/files/{file_id}",
            "delete": "delete /v1/files/{file_id}",
            "content": "get /v1/files/{file_id}/content",
        },
    },
    "batches": {
        "methods": {
            "create": "post /v1/batches",
            "list": "get /v1/batches",
            "retrieve": "get /v1/batches/{batch_id}",
            "cancel": "post /v1/batches/{batch_id}/cancel",
        }
    },
    "alpha": {
        "subresources": {
            "inference": {"methods": {"rerank": "post /v1alpha/inference/rerank"}},
            "post_training": {
                "models": {
                    "algorithm_config": "AlgorithmConfig",
                    "post_training_job": "PostTrainingJob",
                    "list_post_training_jobs_response": "ListPostTrainingJobsResponse",
                },
                "methods": {
                    "preference_optimize": "post /v1alpha/post-training/preference-optimize",
                    "supervised_fine_tune": "post /v1alpha/post-training/supervised-fine-tune",
                },
                "subresources": {
                    "job": {
                        "methods": {
                            "artifacts": "get /v1alpha/post-training/job/artifacts",
                            "cancel": "post /v1alpha/post-training/job/cancel",
                            "status": "get /v1alpha/post-training/job/status",
                            "list": {
                                "paginated": False,
                                "endpoint": "get /v1alpha/post-training/jobs",
                            },
                        }
                    }
                },
            },
            "benchmarks": {
                "models": {
                    "benchmark": "Benchmark",
                    "list_benchmarks_response": "ListBenchmarksResponse",
                },
                "methods": {
                    "retrieve": "get /v1alpha/eval/benchmarks/{benchmark_id}",
                    "list": {
                        "paginated": False,
                        "endpoint": "get /v1alpha/eval/benchmarks",
                    },
                    "register": "post /v1alpha/eval/benchmarks",
                    "unregister": "delete /v1alpha/eval/benchmarks/{benchmark_id}",
                },
            },
            "eval": {
                "models": {
                    "evaluate_response": "EvaluateResponse",
                    "benchmark_config": "BenchmarkConfig",
                    "job": "Job",
                },
                "methods": {
                    "evaluate_rows": "post /v1alpha/eval/benchmarks/{benchmark_id}/evaluations",
                    "run_eval": "post /v1alpha/eval/benchmarks/{benchmark_id}/jobs",
                    "evaluate_rows_alpha": "post /v1alpha/eval/benchmarks/{benchmark_id}/evaluations",
                    "run_eval_alpha": "post /v1alpha/eval/benchmarks/{benchmark_id}/jobs",
                },
                "subresources": {
                    "jobs": {
                        "methods": {
                            "cancel": "delete /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}",
                            "status": "get /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}",
                            "retrieve": "get /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}/result",
                        }
                    }
                },
            },
        }
    },
    "beta": {
        "subresources": {
            "datasets": {
                "models": {"list_datasets_response": "ListDatasetsResponse"},
                "methods": {
                    "register": "post /v1beta/datasets",
                    "retrieve": "get /v1beta/datasets/{dataset_id}",
                    "list": {"paginated": False, "endpoint": "get /v1beta/datasets"},
                    "unregister": "delete /v1beta/datasets/{dataset_id}",
                    "iterrows": "get /v1beta/datasetio/iterrows/{dataset_id}",
                    "appendrows": "post /v1beta/datasetio/append-rows/{dataset_id}",
                },
            }
        }
    },
}


HTTP_METHODS = {"get", "post", "put", "patch", "delete", "options", "head"}


@dataclass
class Endpoint:
    method: str
    path: str
    extra: dict[str, Any] = field(default_factory=dict)

    @classmethod
    def from_config(cls, value: Any) -> Endpoint:
        if isinstance(value, str):
            method, _, path = value.partition(" ")
            return cls._from_parts(method, path)
        if isinstance(value, dict) and "endpoint" in value:
            method, _, path = value["endpoint"].partition(" ")
            extra = {k: v for k, v in value.items() if k != "endpoint"}
            endpoint = cls._from_parts(method, path)
            endpoint.extra.update(extra)
            return endpoint
        raise ValueError(f"Unsupported endpoint value: {value!r}")

    @classmethod
    def _from_parts(cls, method: str, path: str) -> Endpoint:
        method = method.strip().lower()
        path = path.strip()
        if method not in HTTP_METHODS:
            raise ValueError(f"Unsupported HTTP method for Stainless config: {method!r}")
        if not path.startswith("/"):
            raise ValueError(f"Endpoint path must start with '/': {path!r}")
        return cls(method=method, path=path)

    def to_config(self) -> Any:
        if not self.extra:
            return f"{self.method} {self.path}"
        data = dict(self.extra)
        data["endpoint"] = f"{self.method} {self.path}"
        return data

    def route_key(self) -> str:
        return f"{self.method} {self.path}"


@dataclass
class Resource:
    models: dict[str, str] | None = None
    methods: dict[str, Endpoint] = field(default_factory=dict)
    subresources: dict[str, Resource] = field(default_factory=dict)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Resource:
        models = data.get("models")
        methods = {name: Endpoint.from_config(value) for name, value in data.get("methods", {}).items()}
        subresources = {name: cls.from_dict(value) for name, value in data.get("subresources", {}).items()}
        return cls(models=models, methods=methods, subresources=subresources)

    def to_config(self) -> dict[str, Any]:
        result: dict[str, Any] = {}
        if self.models:
            result["models"] = self.models
        if self.methods:
            result["methods"] = {name: endpoint.to_config() for name, endpoint in self.methods.items()}
        if self.subresources:
            result["subresources"] = {name: resource.to_config() for name, resource in self.subresources.items()}
        return result

    def collect_endpoint_paths(self) -> set[str]:
        paths = {endpoint.route_key() for endpoint in self.methods.values()}
        for subresource in self.subresources.values():
            paths.update(subresource.collect_endpoint_paths())
        return paths

    def iter_endpoints(self, prefix: str) -> Iterator[tuple[str, str]]:
        for method_name, endpoint in self.methods.items():
            label = f"{prefix}.{method_name}" if prefix else method_name
            yield endpoint.route_key(), label
        for sub_name, subresource in self.subresources.items():
            sub_prefix = f"{prefix}.{sub_name}" if prefix else sub_name
            yield from subresource.iter_endpoints(sub_prefix)


_RESOURCES = {name: Resource.from_dict(data) for name, data in ALL_RESOURCES.items()}


def _load_openapi_paths(openapi_path: Path) -> set[str]:
    spec = yaml.safe_load(openapi_path.read_text()) or {}
    paths: set[str] = set()
    for path, path_item in (spec.get("paths") or {}).items():
        if not isinstance(path_item, dict):
            continue
        for method, operation in path_item.items():
            if not isinstance(operation, dict):
                continue
            paths.add(f"{str(method).lower()} {path}")
    return paths


@dataclass(frozen=True)
class StainlessConfig:
    organization: dict[str, Any]
    security: list[Any]
    security_schemes: dict[str, Any]
    targets: dict[str, Any]
    client_settings: dict[str, Any]
    environments: dict[str, Any]
    pagination: list[dict[str, Any]]
    settings: dict[str, Any]
    openapi: dict[str, Any]
    readme: dict[str, Any]
    resources: dict[str, Resource]

    @classmethod
    def make(cls) -> StainlessConfig:
        return cls(
            organization=ORGANIZATION,
            security=SECURITY,
            security_schemes=SECURITY_SCHEMES,
            targets=TARGETS,
            client_settings=CLIENT_SETTINGS,
            environments=ENVIRONMENTS,
            pagination=PAGINATION,
            settings=SETTINGS,
            openapi=OPENAPI,
            readme=README,
            resources=dict(_RESOURCES),
        )

    def referenced_paths(self) -> set[str]:
        paths: set[str] = set()
        for resource in self.resources.values():
            paths.update(resource.collect_endpoint_paths())
        paths.update(self.readme_endpoint_paths())
        return paths

    def readme_endpoint_paths(self) -> set[str]:
        example_requests = self.readme.get("example_requests", {}) if self.readme else {}
        paths: set[str] = set()
        for entry in example_requests.values():
            endpoint = entry.get("endpoint") if isinstance(entry, dict) else None
            if isinstance(endpoint, str):
                method, _, route = endpoint.partition(" ")
                method = method.strip().lower()
                route = route.strip()
                if method and route:
                    paths.add(f"{method} {route}")
        return paths

    def endpoint_map(self) -> dict[str, list[str]]:
        mapping: dict[str, list[str]] = {}
        for resource_name, resource in self.resources.items():
            for route, label in resource.iter_endpoints(resource_name):
                mapping.setdefault(route, []).append(label)
        return mapping

    def validate_unique_endpoints(self) -> None:
        duplicates: dict[str, list[str]] = {}
        for route, labels in self.endpoint_map().items():
            top_levels = {label.split(".", 1)[0] for label in labels}
            if len(top_levels) > 1:
                duplicates[route] = labels
        if duplicates:
            formatted = "\n".join(
                f"  - {route} defined in: {', '.join(sorted(labels))}" for route, labels in sorted(duplicates.items())
            )
            raise ValueError("Duplicate endpoints found across resources:\n" + formatted)

    def validate_readme_endpoints(self) -> None:
        resource_paths: set[str] = set()
        for resource in self.resources.values():
            resource_paths.update(resource.collect_endpoint_paths())
        missing = sorted(path for path in self.readme_endpoint_paths() if path not in resource_paths)
        if missing:
            formatted = "\n".join(f"  - {path}" for path in missing)
            raise ValueError("README example endpoints are not present in Stainless resources:\n" + formatted)

    def to_dict(self) -> dict[str, Any]:
        cfg: dict[str, Any] = {}
        for section in SECTION_ORDER:
            if section == "resources":
                cfg[section] = {name: resource.to_config() for name, resource in self.resources.items()}
                continue
            cfg[section] = getattr(self, section)
        return cfg

    def validate_against_openapi(self, openapi_path: Path) -> None:
        if not openapi_path.exists():
            raise FileNotFoundError(f"OpenAPI spec not found at {openapi_path}")
        spec_paths = _load_openapi_paths(openapi_path)
        config_paths = self.referenced_paths()
        missing = sorted(path for path in config_paths if path not in spec_paths)
        if missing:
            formatted = "\n".join(f"  - {path}" for path in missing)
            raise ValueError("Stainless config references missing endpoints:\n" + formatted)

    def validate(self, openapi_path: Path | None = None) -> None:
        self.validate_unique_endpoints()
        self.validate_readme_endpoints()
        if openapi_path is not None:
            self.validate_against_openapi(openapi_path)


def build_config() -> dict[str, Any]:
    return StainlessConfig.make().to_dict()


def write_config(repo_root: Path, openapi_path: Path | None = None) -> Path:
    stainless_config = StainlessConfig.make()
    spec_path = (openapi_path or (repo_root / "client-sdks" / "stainless" / "openapi.yml")).resolve()
    stainless_config.validate(spec_path)
    yaml_text = yaml.safe_dump(stainless_config.to_dict(), sort_keys=False)
    output = repo_root / "client-sdks" / "stainless" / "config.yml"
    output.write_text(HEADER + yaml_text)
    return output


def main() -> None:
    repo_root = Path(__file__).resolve().parents[3]
    output = write_config(repo_root)
    print(f"Wrote Stainless config: {output}")


if __name__ == "__main__":
    main()
